转载自:https://blog.youkuaiyun.com/qq_35494808/article/details/81537415
1、在pom.xml中添加security和jwt的相关依赖,并在启动类上添加注解@EnableWebSecurity
<!-- 权限相关依赖(security和jwt)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、在application.yml中配置mysql及jwt等
server:
port: 9696
servlet:
context-path: /demo
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/security-jwt?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 12345
jpa:
show-sql: true
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
type-aliases-package: com.lan.demo.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.lan.demo.mapper: debug
jwt:
tokenHeader: Authorization
tokenPrefix: Bearer
secret: lanjwt
expiration: 3600
rememberExpiration: 604800
3、新建用户实体类,实现userDetails的方法,用于用户登录的授权验证
/**
* @author: Lan
* @date: 2019/4/9 11:28
* @description:登录成功返回
*/
@Data
public class LoginSuccessVO {
/**
* 用户编号
*/
private String userId;
/**
* 用户手机号码
*/
private String userPhone;
/**
* 角色信息
*/
private List<String> roles;
/**
* 用户名
*/
private String name;
}
/**
* @author: Lan
* @date: 2019/4/8 14:07
* @description:用于校验的用户对象
*/
@Data
public class UserDTO extends LoginSuccessVO implements UserDetails {
/**
* 是否记住密码
*/
private Boolean remember;
/**
* 用户名
*/
private String userName;
/**
* 用户密码
*/
private String userPassword;
/**
* 获取权限信息
*
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities =
getRoles().stream().map(roleName -> new SimpleGrantedAuthority("ROLE_" + roleName)).collect(Collectors.toList());
return grantedAuthorities;
}
@Override
public String getPassword() {
return userPassword;
}
@Override
public String getUsername() {
return userName;
}
/**
* 账户是否未过期
*
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是否未锁定
*
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 账户凭证是否未过期
*
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
4、在业务逻辑层重写UserDetailsService的loadUserByUsername方法,按实际需求来写相对应的“验证规则”即登录成功的评判或标准,返回实现UserDetails的userDTO对象
/**
* @author: Lan
* @date: 2019/4/8 15:24
* @description:
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String json) throws UsernameNotFoundException {
LoginUser loginUser = JSON.parseObject(json, LoginUser.class);
UserDTO userDTO = userMapper.getUserDTO(loginUser.getUserPhone());
if (userDTO == null) {
return null;
}
userDTO.setRemember(loginUser.getRemember());
userDTO.setName(userDTO.getUsername());
userDTO.setUserName(json);
return userDTO;
}
}
5、新建JWTLoginFilter类,该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 attemptAuthentication 和successfulAuthentication ,当登录成功后,生成一个token,并将token返回给客户端。
/**
* @author: Lan
* @date: 2019/4/8 15:27
* @description:处理登录请求
*/
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserMapper userMapper;
private AuthenticationManager authenticationManager;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
/**
* 请求登录
*
* @param request
* @param response
* @return
* @throws AuthenticationException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
LoginForm loginForm = new ObjectMapper().readValue(request.getInputStream(), LoginForm.class);
checkLoginForm(loginForm, response);
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(loginForm, loginUser);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(JSON.toJSONString(loginUser), loginForm.getUserPassword(), new ArrayList<>()));
} catch (IOException e) {
ResponseUtil.write(response, ResultUtil.error("数据读取错误"));
}
return null;
}
/**
* 登录成功后
*
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserDTO userDTO = (UserDTO) authResult.getPrincipal();
if (jwtTokenUtil == null) {
jwtTokenUtil = (JwtTokenUtil) SpringUtils.getBean("jwtTokenUtil");
}
User user = new User();
user.setUserId(userDTO.getUserId());
user.setUserLastLoginTime(TimeUtil.nowTimeStamp());
if (userMapper == null) {
userMapper = (UserMapper) SpringUtils.getBean("userMapper");
}
//更新登最近一次录时间
userMapper.updateById(user);
String token = jwtTokenUtil.createToken(userDTO);
//将token放置请求头返回
response.addHeader(jwtTokenUtil.getTokenHeader(), jwtTokenUtil.getTokenPrefix() + token);
LoginSuccessVO loginSuccessVO = new LoginSuccessVO();
BeanUtils.copyProperties(userDTO, loginSuccessVO);
ResponseUtil.write(response, ResultUtil.success(loginSuccessVO));
}
/**
* 登录失败
*
* @param request
* @param response
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.write(response, ResultUtil.error(failed.getMessage()));
}
/**
* 校验参数
*
* @param loginForm
*/
private void checkLoginForm(LoginForm loginForm, HttpServletResponse response) {
if (BlankUtil.isBlank(loginForm.getUserPhone())) {
ResponseUtil.write(response, ResultUtil.error("手机号不能为空"));
return;
}
if (BlankUtil.isBlank(loginForm.getUserPassword())) {
ResponseUtil.write(response, ResultUtil.error("密码不能为空"));
return;
}
}
}
6、新建JWTAuthenticationFilter类,该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。 如果校验通过,就认为这是一个取得授权的合法请求。在这也可以实现刷新token的操作(可以看情况加上去)。
/**
* @author: Lan
* @date: 2019/4/8 15:28
* @description:token 校验过滤器
*/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
if (jwtTokenUtil == null) {
jwtTokenUtil = (JwtTokenUtil) SpringUtils.getBean("jwtTokenUtil");
}
String header = request.getHeader(jwtTokenUtil.getTokenHeader());
//当token为空或格式错误时 直接放行
if (header == null || !header.startsWith(jwtTokenUtil.getTokenPrefix())) {
chain.doFilter(request, response);
return;
}
UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(header);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
}
/**
* 这里从token中获取用户信息并新建一个token
*/
private UsernamePasswordAuthenticationToken getAuthentication(String header) {
String token = header.replace(jwtTokenUtil.getTokenPrefix(), "");
String principal = jwtTokenUtil.getUserName(token);
if (principal != null) {
UserDTO userDTO = jwtTokenUtil.getUserDTO(token);
return new UsernamePasswordAuthenticationToken(principal, null, userDTO.getAuthorities());
}
return null;
}
}
7、要实现通过角色动态控制url权限
/**
* @author: Lan
* @date: 2019/4/9 14:56
* @description:判断是否具有权限访问当前资源
*/
@Component("rbacauthorityservice")
public class RbacAuthorityService {
@Autowired
private PermissionMapper permissionMapper;
/**
* 判断是否有权限
*
* @param request
* @param authentication
* @return
*/
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Collection<ConfigAttribute> collection = getAttributes(request);
if (authentication.getPrincipal().equals("anonymousUser")) {
return false;
}
if (null == collection || collection.size() <= 0) {
return true;
}
ConfigAttribute configAttribute;
String needRole;
for (Iterator<ConfigAttribute> iterator = collection.iterator(); iterator.hasNext(); ) {
configAttribute = iterator.next();
needRole = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (needRole.trim().equals(grantedAuthority.getAuthority())) {
return true;
}
}
}
throw new AccessDeniedException("权限不足");
}
/**
* 判定用户请求的url是否在权限表中,如果在权限表中,则返回decide方法,
* 用来判定用户是否有权限,如果不在权限表中则放行
*
* @param request
* @return
* @throws IllegalArgumentException
*/
public Collection<ConfigAttribute> getAttributes(HttpServletRequest request) throws IllegalArgumentException {
HashMap<String, Collection<ConfigAttribute>> map = PermissionMap.map;
if (map == null) {
map = loadResourceDefine(map);
}
for (Map.Entry<String, Collection<ConfigAttribute>> entry : map.entrySet()) {
String url = entry.getKey();
if (new AntPathRequestMatcher(url).matches(request)) {
return map.get(url);
}
}
return null;
}
/**
* 加载权限表中所有权限
*/
private HashMap<String, Collection<ConfigAttribute>> loadResourceDefine(HashMap<String, Collection<ConfigAttribute>> map) {
map = new HashMap<>();
List<PermissionDto> all = permissionMapper.findAll();
for (PermissionDto permissionDto : all) {
List<ConfigAttribute> configAttributeList = permissionDto.getRoleNames().stream().map(roleName -> {
ConfigAttribute configAttribute = new SecurityConfig("ROLE_" + roleName.toUpperCase());
return configAttribute;
}).collect(Collectors.toList());
map.put(permissionDto.getPermissionUrl(), configAttributeList);
}
PermissionMap.map = map;
return map;
}
}
8、新建WebSecurityConfig类配置springsecurity,通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起等
/**
* @author: Lan
* @date: 2019/4/8 15:22
* @description:security配置
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/user/hello").permitAll()
.anyRequest().authenticated()
.anyRequest()
.access("@rbacauthorityservice.hasPermission(request,authentication)")
.and()
.addFilter(new JwtLoginFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.exceptionHandling().accessDeniedHandler(new RestAuthenticationAccessDeniedHandler())
.authenticationEntryPoint(new AuthEntryPoint());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
9.注销登录,将资源数据源赋值为null,建议在登录的时候加入redis存取token,注销时可以将对应token清除,token达到无效的作用,在校验token时除了校验token合法性还需与redis中比较。
public ResultUtil logout(HttpServletRequest request, HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
PermissionMap.map = null;
PermissionMap.list = null;
}
return ResultUtil.success();
}