Spring Boot 使用 Mybatis
依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
使用:
啟動類上注解:@MapperScan("com.dao"),或者每個 dao 接口上添加注解:@Mapper
@Mapper 是 MyBatis 的注解,目的是為了讓 spring 能夠根據 xml 和這個接口動態生成接口的實現。
@Mapper
public interface UserDao {
@Select("select * from user where id=#{id}")
User GetUserbyId(@Param("id") Integer id);
}
注解方式:
@Select 是查詢類的注解,所有的查詢均使用這個
@Result 修飾返回的結果集,關聯實體類屬性和數據庫字段一一對應,如果實體類屬性和數據庫屬性名保持一致,就不需要這個屬性來修飾。
@Insert 插入數據庫使用,直接傳入實體類會自動解析屬性到對應的值
@Update 負責修改,也可以直接傳入對象
@Delete 負責刪除
mapper.xml 方式:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dao.UserDao">
<!-- 可根據自己的需求,是否要使用 -->
<resultMap type="User" id="UserDaoMap">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="userName" property="useName" jdbcType="VARCHAR" />
</resultMap>
<select id="GetUserbyId" parameterType="INTEGER" resultMap="UserDaoMap">
select *
from user
where 1=1
and id = #{id,jdbcType=INTEGER}
</select>
</mapper>
namespace 指定這個 xml 所對應的是哪個 dao 接口。
select 標簽的 id 對應的就是 dao 接口中的方法名,parameterType 就是傳進來的參數類型。
Mysql 去掉 ONLY_FULL_GROUP_BY
ONLY_FULL_GROUP_BY:
對於GROUP_BY聚合操作,如果在SELECT中的列既沒有在GROUP_BY中出現,
本身也不是聚合列(使用SUM,ANG等修飾的列),那么這句SQL是不合法的,因為那一列是不確定的。
SELECT @@global.sql_mode;
SET @@global.sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
Spring Security oAuth2
Spring Security 基於 Servlet 過濾器、IoC 和 AOP,
為 Web 請求和方法調用提供身份確認和授權處理,避免了代碼耦合,減少了大量重復代碼工作。
oAuth2 是授權標准,協議,體現在代碼上是接口。實現有 Spring Security 和 Shire。
Access Token 是客戶端訪問資源服務器的令牌(Access Token 臨時的,有一定有效期)。
Refresh Token 的作用是用來刷新 Access Token(Refresh Token 效期非常長)。
刷新接口類似於:http://www.funtl.com/refresh?refresh_token=&client_id=&client_secret=
認證:Authentication 授權:Authorization
認證(Authentication):確定一個用戶的身份的過程。
授權(Authorization):判斷一個用戶是否有訪問某個安全對象的權限。
認證與授權中關鍵的三個過濾器:
1. UsernamePasswordAuthenticationFilter:
該過濾器用於攔截我們表單提交的請求(默認為/login),進行用戶的認證過程。
2. ExceptionTranslationFilter:
該過濾器主要用來捕獲處理 Spring Security 拋出的異常,異常主要來源於 FilterSecurityInterceptor。
3. FilterSecurityInterceptor:
該過濾器主要用來進行授權判斷。
Spring Security 核心組件
Spring Security 核心組件有:
SecurityContext
SecurityContextHolder
Authentication
Userdetails
AuthenticationManager
SecurityContext(安全上下文):
當用戶通過認證之后,就會為這個用戶生成一個唯一的SecurityContext,里面包含用戶的認證信息Authentication。
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication var1);
}
SecurityContextHolder(安全上下文持有者):
在SecurityContextHolder中保存的是當前訪問者的信息。Spring Security使用一個Authentication對象來表示這個信息。
缺省情況下,SecurityContextHolder使用了ThreadLocal機制來保存每個使用者的安全上下文。
SecurityContextHolder可以工作在以下三種模式之一:
MODE_THREADLOCAL (缺省工作模式)
MODE_GLOBAL(所有線程中都相同)
MODE_INHERITABLETHREADLOCAL(存儲在線程中,但子線程可以獲取到父線程中的 SecurityContext)
修改SecurityContextHolder的工作模式有兩種方法:
設置一個系統屬性(system.properties):spring.security.strategy
調用SecurityContextHolder靜態方法:setStrategyName()
存儲當前認證信息:
SecurityContextHolder.getContext().setAuthentication(token);
獲取保存在ThreadLocal中的用戶信息:
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Authentication(認證):
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities(); //獲取用戶權限信息
Object getCredentials(); // 獲取認證信息
Object getDetails(); //獲取用戶描述信息
Object getPrincipal(); //獲取用戶身份信息,在未認證的情況下獲取到的是用戶名,在已認證的情況下獲取到的是 UserDetails
boolean isAuthenticated(); //是否已經認證
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetailsService
UserDetailsService也是一個接口,且只有一個方法loadUserByUsername,用來獲取UserDetails。
AuthenticationManager
AuthenticationManager 是一個接口,它只有一個方法,接收參數為Authentication,其定義如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 的作用就是校驗 Authentication,
如果驗證失敗會拋出 AuthenticationException 異常。
pre-post-annotations
Spring Security 中定義了四個支持使用表達式的注解,分別是:
@PreAuthorize
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
@PreAuthorize("#user.name.equals('abc')")
@PostAuthorize
@PostAuthorize 是在方法調用完成后進行權限檢查,
它不能控制方法是否能被調用,只能在方法調用完成后檢查權限決定是否要拋出 AccessDeniedException。
@PreFilter
@PreFilter(filterTarget="ids", value="filterObject%2==0"):保留ids集合中的偶數
@PostFilter
其中前兩者可以用來在方法調用前或者調用后進行權限檢查,
后兩者可以用來對集合類型的參數或者返回值進行過濾。
hasRole 和 hasAuthority
角色授權:授權代碼需要加 ROLE_ 前綴,Controller 上使用時不要加前綴。
權限授權:設置和使用時,名稱保持一至即可。
使用流程
WebSecurityConfig(配置文件)
@Configuration
/**
* @EnableWebSecurity注解有兩個作用:
* 1: 加載了WebSecurityConfiguration, 配置安全策略。
* 2: 加載了AuthenticationConfiguration, 配置了認證信息。
*/
@EnableWebSecurity
/**
* prePostEnabled = true:
* 使用表達式時間方法級別的安全性
* 4個注解可用:@PreAuthorize等
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Bean
public JwtTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtTokenFilter();
}
/**
* 用於認證過程中載入用戶信息:public class UserService implements UserDetailsService
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
/**
* SpringSecurity會把匹配規則按注冊先后順序放到一個ArrayList<UrlMapping>中,
* 先注冊的規則放前面,后注冊的放后面。
*/
httpSecurity
//禁用csrf
.csrf().disable()
/**
* 有多個配置HttpSecurity的配置類時必須設置.antMatcher("/**"),
* 因為不設置的話默認匹配所有,就不會執行到下面的HttpSecurity了。
*
* 需要在類上加注解@Order,指明優先級
*/
.antMatcher("/**")
//設置無狀態SessionCreationPolicy(不需要Session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//登陸頁
.formLogin()
.loginPage("/login") //登陸頁
.failureUrl("/login") //登陸失敗的頁面跳轉URL
.defaultSuccessUrl("/index") //登錄成功后的默認處理頁
.permitAll()
.and()
//注銷頁
.logout()
.logoutUrl("/logout") //注銷接口
.logoutSuccessUrl("/login") //注銷成功跳轉接口
.permitAll()
.and()
//通過 authorizeRequests() 定義哪些URL需要被保護、哪些不需要被保護
.authorizeRequests()
//OPTIONS請求全部放行
.antMatchers(HttpMethod.OPTIONS).permitAll()
//其他 post put delete get 請求全部攔截校驗
.antMatchers(HttpMethod.POST).authenticated() //需要認證
.antMatchers(HttpMethod.PUT).authenticated()
.antMatchers(HttpMethod.DELETE).authenticated()
.antMatchers(HttpMethod.GET).authenticated();
/**
* addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
* 在 beforeFilter 之前添加 filter。
*
* UsernamePasswordAuthenticationFilter:登陸用戶密碼驗證過濾器
*/
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
/**
* 禁用默認響應安全頭,添加要用的響應安全頭:
* httpSecurity.headers().defaultsDisabled().cacheControl();
*/
httpSecurity.headers().cacheControl();
}
}
JwtTokenFilter過濾器
每次請求都經過過濾器,判斷是否持有Token(已經登陸),如果持有Token就進行認證,否則以游客身份認證。
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
@Autowired
private UserService userService;
@Autowired
private JwtConfig jwtConfig;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
boolean giveFlag = false;
//從請求頭中獲取token
String authHeader = request.getHeader(jwtConfig.getHeader());
//認證
if (authHeader != null && authHeader.startsWith(jwtConfig.getPrefix())) {
//如果token存在,並且格式正確,就從數據庫查詢UserDetails
UserDetails userDetails = userService.loadUserByToken(authHeader);
if (null != userDetails) {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//用戶存在,本次會話的權限還未被寫入
//生成認證信息
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//將認證信息寫入本次會話
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} else {
//用戶不存在,以游客身份認證
giveFlag = true;
}
} else {
//token不存在或者格式錯誤,以游客身份認證
giveFlag = true;
}
//給游客授權
if (giveFlag) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("NORMAL"));
User user = new User("NORMAL", "NORMAL", authorities);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
UserService
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Autowired
private RoleDao roleDao;
@Autowired
private HttpServletRequest request;
@Autowired
private BCryptPasswordEncoder encoder;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private JwtConfig jwtConfig;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 登錄(校驗User,通過校驗則生成Token並放入Redis,並返回Token)
*/
public String login(User user) {
User dbUser = userDao.findUserByName(user.getName());
//用戶名或密碼錯誤
if (null == dbUser || !encoder.matches(user.getPassword(), dbUser.getPassword())) {
throw new UsernameNotFoundException("用戶名或密碼錯誤");
}
//用戶已被封禁
if (0 == dbUser.getState()) {
throw new RuntimeException("你已被封禁");
}
//生成Token
final UserDetails userDetails = this.loadUserByUsername(user.getName());
final String token = jwtTokenUtil.generateToken(userDetails);
//key:TOKEN_username,value:Bearer token
redisTemplate.opsForValue().
set(JwtConfig.REDIS_TOKEN_KEY_PREFIX + user.getName(), jwtConfig.getPrefix() + token, jwtConfig.getTime(), TimeUnit.SECONDS);
return token;
}
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
User user = userDao.findUserByName(name);
List<SimpleGrantedAuthority> authorities = new ArrayList<>(1);
List<Role> roles = roleDao.findUserRoles(user.getId());
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new org.springframework.security.core.userdetails.User(user.getName(), null, authorities);
}
/**
* 從Token中提取信息,校驗Token
*/
public UserDetails loadUserByToken(String authHeader) {
//除去前綴,獲取Token
final String authToken = authHeader.substring(jwtConfig.getPrefix().length());
String username = jwtTokenUtil.getUsernameFromToken(authToken);
//Token非法
if (null == username) {
return null;
}
//通過username從緩存中獲取Token:判斷Token是否過期,或者Token是否匹配
String redisToken = redisTemplate.opsForValue().get(JwtConfig.REDIS_TOKEN_KEY_PREFIX + username);
if (!authHeader.equals(redisToken)) {
return null;
}
List<String> roles = jwtTokenUtil.getRolesFromToken(authToken);
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return new org.springframework.security.core.userdetails.User(username, null, authorities);
}
/**
* 退出登錄(清除Redis緩存中的Token)
*/
public void logout() {
String username = jwtTokenUtil.getUsernameFromRequest(request);
redisTemplate.delete(JwtConfig.REDIS_TOKEN_KEY_PREFIX + username);
}
}
JwtTokenUtil
public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {
String ISSUER = "iss";
String SUBJECT = "sub";
String AUDIENCE = "aud";
String EXPIRATION = "exp";
String NOT_BEFORE = "nbf";
String ISSUED_AT = "iat";
String ID = "jti";
}
@ConfigurationProperties(prefix = "jwt")
@Component
@Data
public class JwtConfig {
public static final String REDIS_TOKEN_KEY_PREFIX = "TOKEN_"; //Redis緩存前綴
private long time = 432000; //5天過期時間
private String secret = "Sara"; //JWT密碼
private String prefix = "Bearer "; //Token前綴
private String header = "Authorization"; //存放Token的Header Key
}
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -5625635588908941275L;
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
private static final String CLAIM_KEY_ROLES = "roles";
@Autowired
private JwtConfig jwtConfig;
/**
* 生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); //放入用戶名 sub
claims.put(CLAIM_KEY_CREATED, new Date()); //放入token生成時間 created
List<String> roles = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
for (GrantedAuthority authority : authorities) {
roles.add(authority.getAuthority());
}
claims.put(CLAIM_KEY_ROLES, roles); //放入用戶權限 roles
return generateToken(claims);
}
/**
* 生成token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getTime() * 1000))
.signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret())
.compact();
}
/**
* 校驗token
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token); //從token中取出用戶名
return (username.equals(userDetails.getUsername())
&&
!isTokenExpired(token) //校驗是否過期
);
}
/**
* 從request中獲取username
*/
public String getUsernameFromRequest(HttpServletRequest request) {
String token = request.getHeader(jwtConfig.getHeader());
token = token.substring(jwtConfig.getPrefix().length());
return token == null ? null : getUsernameFromToken(token);
}
/**
* 從token中獲取username
*/
public String getUsernameFromToken(String token) {
String username;
try {
final Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 從token中獲取Claims
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(jwtConfig.getSecret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 從token中獲取創造時間
*/
public Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = new Date((Long)claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}
/**
* token是否過期
*
* true 過期 false 未過期
*/
public Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
boolean isExpired = expiration.before(new Date());
return isExpired;
}
/**
* 從token中獲取過期時間
*/
public Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}
/**
* 從token中獲取用戶角色
*/
public List<String> getRolesFromToken(String authToken) {
List<String> roles;
try {
final Claims claims = getClaimsFromToken(authToken);
roles = (List<String>)claims.get(CLAIM_KEY_ROLES);
} catch (Exception e) {
roles = null;
}
return roles;
}
}
