一.shiro
1.realm:这里用了ehcache,其实这里用法时错误的,结合jwt就应该彻底抛弃后台状态(同事写的..)
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
CacheManager cacheManager;
@Autowired
UserVo userVo;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.debug("————权限认证————");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("user");
if(userVo.getUserType()== UserType.ADMIN.getType()){
info.addRole("admin");
}
//获得该用户角色
return info;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.debug("————身份认证方法————");
String token = (String) authenticationToken.getCredentials();
userVo = cacheManager.getCache("auth").get(token, UserVo.class);
if(userVo==null){
throw new AuthenticationException("token认证失败!");
}else{
cacheManager.getCache("auth").put(token,userVo);
}
return new SimpleAuthenticationInfo(token, token, "MyRealm");
}
}
2.filter:
public class JWTFilter extends BasicHttpAuthenticationFilter {
public final static String AUTH_KEY = "token";
public final static int NOT_LOGIN = 1;
public final static int NOT_AUTH = 2;
/**
* 进行token检测
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//判断请求的请求头是否带上 "Token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
}catch(Exception ex){
redirect(NOT_LOGIN, response);
}
return false;
}
redirect(NOT_LOGIN, response);
return false;
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(AUTH_KEY);
String requestURI = req.getRequestURI();
return token != null;
}
/**
* 执行登陆操作
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws AuthenticationException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(AUTH_KEY);
JWTToken jwtToken = new JWTToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
private void redirect(int type, ServletResponse response){
Response response1 = null;
if(type==NOT_LOGIN){
response1 = Response.notLogin("未登录");
}else if(type==NOT_AUTH){
response1 = Response.notAuth("没有权限");
}
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(response1));
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
public class JWTToken implements AuthenticationToken {
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
3.总配置:(这里的过滤器要使用new方式用shiro管理,不能注入spring,否则因为顺序原因导致授权失败)
注意:此处过滤链使用的是linkedMap,保证顺序;
@Configuration
@Slf4j
public class Security {
@Bean
public ShiroFilterFactoryBean shirFilter(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<>();
//设置我们自定义的JWT过滤器
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
Map<String, String> filterRuleMap = new LinkedHashMap<>();
// 访问 /unauthorized 不通过JWTFilter
filterRuleMap.put("/v2/api-docs/**", "anon");
filterRuleMap.put("/swagger-resources/**", "anon");
filterRuleMap.put("/swagger-ui.html*", "anon");
filterRuleMap.put("/webjars/**", "anon");
filterRuleMap.put("/images/**", "anon");
filterRuleMap.put("/configuration/**", "anon");
filterRuleMap.put("/not_login", "anon");
filterRuleMap.put("/login", "anon");
// 所有请求通过我们自己的JWT Filter
filterRuleMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
@Autowired
MyRealm myRealm;
/**
* 注入 securityManager
*/
@Bean
public DefaultWebSecurityManager manager(MyRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
/**
* 自定义身份认证 realm;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
* 否则会影响 CustomRealm类 中其他类的依赖注入
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* 权限注解
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
注解:
@RequiresRoles("admin")等
二.springscurity
1.userDetails:
@Data
public class MyUserDetails implements UserDetails {
private String account;
private String password;
private Set<GrantedAuthority> set;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.set;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.account;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
2.service:
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String token) throws UsernameNotFoundException {
MyUserDetails userDetails = null;
try {
TUser tUser = JSON.parseObject(token, TUser.class);
userDetails = new MyUserDetails();
userDetails.setAccount(tUser.getAccount());
userDetails.setPassword(tUser.getPassword());
Integer role = tUser.getRole();
Set<GrantedAuthority> set = new HashSet<>();
set.add((new SimpleGrantedAuthority("ROLE_user")));
if (role != null && role == 2) {
set.add((new SimpleGrantedAuthority("ROLE_admin")));
}
userDetails.setSet(set);
} catch (Exception e) {
log.error("json error");
}
return userDetails;
}
}
3.filter:
@Component
public class MyFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("token");
if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
String info = JWTUtil.getInfo(token);
boolean verify = JWTUtil.verify(token, info);
if (verify) {
UserDetails userDetails = myUserDetailsService.loadUserByUsername(info);
if (userDetails != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
4.总配置:
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyFilter myFilter;
@Autowired
private MyUserDetailsService myUserDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().disable().exceptionHandling()
.authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
ret(httpServletResponse, true);
})
.accessDeniedHandler((httpServletRequest, httpServletResponse, e) -> {
ret(httpServletResponse, false);
})
.and()
.authorizeRequests()
.antMatchers("/login", "/swagger-ui.html*"
, "/swagger-resources/**", "/webjars/**", "/configuration/**"
, "/images/**", "/v2/api-docs/**")
.permitAll()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
public void ret(HttpServletResponse httpServletResponse, boolean flag) throws IOException {
httpServletResponse.setContentType("application/json;charset=utf-8");
if (flag) {
httpServletResponse.getWriter().write(JSON.toJSONString(MyResponse.notLogin("登陆失败")));
} else {
httpServletResponse.getWriter().write(JSON.toJSONString(MyResponse.notAuth("没有权限")));
}
}
}
注解:
@EnableGlobalMethodSecurity :Spring Security默认是禁用注解的,要想开启注解,需要在继承WebSecurityConfigurerAdapter的类上加@EnableGlobalMethodSecurity注解,并在该类中将AuthenticationManager定义为Bean。
一、JSR-250注解
@DenyAll 拒绝所有访问
@RolesAllowed({"USER", "ADMIN"}) 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省 略前缀ROLE_,实际的权限可能是ROLE_ADMIN
@PermitAll 允许所有访问
二、prePostEnabled注解
@PreAuthorize:在方法执行之前执行,而且这里可以调用方法的参数,也可以得到参数值,这是利用JAVA8的参数名反射特性,如果没用JAVA8,那么也可以利用Spring Security的@P标注参数,或者Spring Data的@Param标注参数。
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
void changePassword(@P("userId") long userId ){ }
这里表示在changePassword方法执行之前,判断方法参数userId的值是否等于principal中保存的当前用户的userId,或者当前用户是否具有ROLE_ADMIN权限,两种符合其一,就可以访问该方法。
@PostAuthorize:在方法执行之后执行,而且这里可以调用方法的返回值,如果EL为false,那么该方法也已经执行完了,可能会回滚。EL变量returnObject表示返回的对象。
@PostAuthorize
User getUser("returnObject.userId == authentication.principal.userId or hasPermission(returnObject, 'ADMIN')");
@PreFilter:在方法执行之前执行,而且这里可以调用方法的参数,然后对参数值进行过滤或处理或修改,EL变量filterObject表示参数,如有多个参数,使用filterTarget注解参数。只有方法参数是集合或数组才行。(很少会用到,与分页技术不兼容)
三、securedEnabled 注解
@Secured认证是否有权限访问,例如
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")public Account readAccount(Long id);
@Secured("ROLE_TELLER")
_________________12.15更新,关闭httpbase,使用lambda表达式响应登录结果
_________________12.25更新,springsecurity配置多provider登陆策略;
(1)配置自定义的token实体,还有两个provider;
public class Token extends AbstractAuthenticationToken {
private String token;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Token(Collection<? extends GrantedAuthority> authorities) {
super(authorities);
}
@Override
public Object getCredentials() {
return token;
}
@Override
public Object getPrincipal() {
return token;
}
这里的token实体是来区分选择不同登陆处理器用的;
@Component
public class OneProvider implements AuthenticationProvider {
private String user = "321";
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
TUser tUser = JSON.parseObject(name, TUser.class);
if (tUser.getAccount().equalsIgnoreCase(user)) {
return new UsernamePasswordAuthenticationToken(name, authentication.getCredentials(), new ArrayList<>());
}
return null;
}
@Override
public boolean supports(Class<?> aClass) {
System.out.println(aClass.getName());
return aClass.equals(UsernamePasswordAuthenticationToken.class);
}
}
@Component
public class TwoProvider implements AuthenticationProvider {
private String user = "123";
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
TUser tUser = JSON.parseObject(name, TUser.class);
if (tUser.getAccount().equalsIgnoreCase(user)) {
return new UsernamePasswordAuthenticationToken(name, authentication.getCredentials(), new ArrayList<>());
}
return null;
}
@Override
public boolean supports(Class<?> aClass) {
System.out.println(aClass.getName());
return aClass.equals(Token.class);
}
}
这里是根据supports进行处理的,返回为true进行登陆处理;
@Component
public class OneFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest
, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = httpServletRequest.getHeader("token");
String type = httpServletRequest.getParameter("type");
if (token != null && SecurityContextHolder.getContext().getAuthentication() == null) {
String info = JWTUtil.getInfo(token);
boolean verify = JWTUtil.verify(token, info);
if (verify) {
if (type == null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(info, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
Token token1 = new Token(new ArrayList<>());
token1.setToken(info);
SecurityContextHolder.getContext().setAuthentication(token1);
}
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
这里可以根据预先设计的登陆类别来判断封装成那种token类型;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptPasswordEncoder());
auth.authenticationProvider(oneProvider);
auth.authenticationProvider(two);
}
下面为配置多个登陆处理器;
12.29;
如果在realm中抛出自定义异常,想要被shiro自定义filter拦截的话,首先抛出的异常需要继承AuthenticationException,其次你的自定义过滤器需要继承 AuthorizationFilter;这样才能被拦截到.
还有如果使用@RequiresRoles()权限注解的话,在配置中配置的setUnauthorizedUrl无权限跳转地址是无法跳转的,因为他只能在过滤链中配置roles等时生效;所以可以使用控制层全局的异常处理;
1.11 修改shiro未登录及无权限返回状态;直接通过respose返回json,否则通过重定向会使前端url改变;并且删除配置中的跳转登陆页面及无权限页面,该处用token前后端分离方式登陆无效;